package ua.soso.core;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Interval;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormatterBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;


public final class DateUtil {
    private static final Logger LOG = LoggerFactory.getLogger(DateUtil.class);
    private static final DateTimeFormatter ISO_PERIOD_FORMATTER = new DateTimeFormatterBuilder().
            appendYear(4, 4).appendLiteral('-').
            appendMonthOfYear(2).appendLiteral('-').
            appendDayOfMonth(2).appendLiteral('T').
            appendHourOfDay(2).appendLiteral(':').
            appendMinuteOfHour(2).appendLiteral('Z').toFormatter().withZone(DateTimeZone.UTC);
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd").withZone(DateTimeZone.getDefault());
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm").withZone(DateTimeZone.getDefault());
    private static final DateTimeFormatter FILE_IDENTIFICATION_FORMATTER = DateTimeFormat.forPattern("yyyyMMddHHmmss");

    public static final DatatypeFactory XML_DATATYPE_FACTORY;
    public static final DateTimeZone ZONE = DateTimeZone.getDefault();
    public static final DateTime EPOCH = new DateTime(0);

    private static DateTime currentDate;

    static {
        try {
            XML_DATATYPE_FACTORY = DatatypeFactory.newInstance();
        } catch (DatatypeConfigurationException e) {
            LOG.error("Data type configuration error", e);
            throw new IllegalStateException(e);
        }
    }

    /**
     *
     * @return current real or simulation date
     */
    public static DateTime getCurrentDate() {
        return currentDate != null ? currentDate : new DateTime();
    }

    /**
     * Converts given {@link XMLGregorianCalendar} to the {@link DateTime}. The result will be in the default time zone
     *
     * @param calendar
     *            XML calendar
     * @return converted {@link DateTime} instance
     * @throws IllegalArgumentException
     *             if the parameter is undefined or the zone is present but cannot be recognised.
     */
    public static DateTime from(XMLGregorianCalendar calendar) throws IllegalArgumentException {
        return fromWithCalendarTZ(calendar).toDateTime(DateTimeZone.getDefault());
    }

    /**
     * Converts given {@link XMLGregorianCalendar} to the {@link DateTime}. The result will be in the time zone of the calendar
     *
     * @param calendar
     *            XML calendar
     * @return converted {@link DateTime} instance
     * @throws IllegalArgumentException
     *             if the parameter is undefined or the zone is present but cannot be recognised.
     */

    public static DateTime fromWithCalendarTZ(XMLGregorianCalendar calendar) throws IllegalArgumentException {
        LOG.trace("Parse XMLGregorianCalendar {}", calendar);

        Assert.notNull(calendar, "Undefined dateTime value");

        int mills = calendar.getMillisecond();

        return new DateTime(calendar.getYear(), calendar.getMonth(), calendar.getDay(),
                calendar.getHour(), calendar.getMinute(), calendar.getSecond(),
                mills == DatatypeConstants.FIELD_UNDEFINED ? 0 : mills,
                DateTimeZone.forTimeZone(calendar.getTimeZone(DatatypeConstants.FIELD_UNDEFINED)));
    }

    /**
     * Converts given {@link DateTime} to the {@link XMLGregorianCalendar}.
     *
     * @param dateTime
     *            Joda Time {@link DateTime} instance
     * @param offset
     *            offset from UTC in hours
     * @return converted XML calendar
     * @throws IllegalArgumentException
     *             if the parameter is undefined.
     */
    public static XMLGregorianCalendar to(DateTime dateTime, int offset) {
        LOG.trace("Format DateTime {}", dateTime);

        Assert.notNull(dateTime, "Undefined dateTime value");

        dateTime = dateTime.toDateTime(DateTimeZone.forOffsetHours(offset));

        XMLGregorianCalendar calendar = XML_DATATYPE_FACTORY.newXMLGregorianCalendar();

        calendar.setYear(dateTime.getYear());
        calendar.setMonth(dateTime.getMonthOfYear());
        calendar.setDay(dateTime.getDayOfMonth());
        calendar.setHour(dateTime.getHourOfDay());
        calendar.setMinute(dateTime.getMinuteOfHour());
        calendar.setSecond(dateTime.getSecondOfMinute());
        calendar.setMillisecond(dateTime.getMillisOfSecond());
        calendar.setTimezone(dateTime.getZone().getOffset(dateTime) / (1000 * 60));

        return calendar;
    }

    /**
     * Converts given {@link DateTime} to the {@link XMLGregorianCalendar} in UTC timezone.
     *
     * @param dateTime
     *            Joda Time {@link DateTime} instance
     * @return converted XML calendar
     * @throws IllegalArgumentException
     *             if the parameter is undefined.
     */
    public static XMLGregorianCalendar to(DateTime dateTime) {
        return to(dateTime, 0);
    }

    /**
     * Converts given {@link DateTime}s to the string representation of the ISO period.
     *
     * @param start
     *            start period {@link DateTime} instance
     * @param stop
     *            end period {@link DateTime} instance
     * @return the ISO period string (e.g. <code>2011-01-02T22:00Z/2011-01-03T22:00Z</code>).
     * @throws IllegalArgumentException
     *             if at least one parameter is undefined.
     */
    public static String formatPeriod(DateTime start, DateTime stop) throws IllegalArgumentException {
        LOG.trace("Format period from ({}) to ({})", start, stop);

        Assert.notNull(start, "Undefined start period value");
        Assert.notNull(stop, "Undefined stop period value");

        return ISO_PERIOD_FORMATTER.print(start) + "/" + ISO_PERIOD_FORMATTER.print(stop);
    }

    /**
     * Converts given {@link XMLGregorianCalendar}s to the string representation of the ISO period.
     *
     * @param start
     *            start period {@link XMLGregorianCalendar} instance
     * @param stop
     *            end period {@link XMLGregorianCalendar} instance
     * @return the ISO period string (e.g. <code>2011-01-02T22:00Z/2011-01-03T22:00Z</code>).
     * @throws IllegalArgumentException
     *             if at least one parameter is undefined.
     */
    public static String formatPeriod(XMLGregorianCalendar start, XMLGregorianCalendar stop) throws IllegalArgumentException {
        LOG.trace("Format period from ({}) to ({})", start, stop);

        Assert.notNull(start, "Undefined start period value");
        Assert.notNull(stop, "Undefined stop period value");

        return ISO_PERIOD_FORMATTER.print(from(start)) + "/" + ISO_PERIOD_FORMATTER.print(from(stop));
    }

    /**
     * Converts given {@link DateTime}s to the string representation of the ISO period.
     *
     * @param interval
     *            period {@link Interval} instance
     * @return the ISO period string (e.g. <code>2011-01-02T22:00Z/2011-01-03T22:00Z</code>).
     * @throws IllegalArgumentException
     *             if at least one parameter is undefined.
     */
    public static String formatPeriod(Interval interval) throws IllegalArgumentException {
        LOG.trace("Format period from ({}) to ({})", interval.getStart(), interval.getEnd());

        return ISO_PERIOD_FORMATTER.print(interval.getStart()) + "/" + ISO_PERIOD_FORMATTER.print(interval.getEnd());
    }

    /**
     * Converts given {@link DateTime} to the string representation of the ISO.
     *
     * @param dateTime
     *            {@link DateTime} instance
     * @return the ISO string (e.g. <code>2011-01-02T22:00Z</code>).
     * @throws IllegalArgumentException
     *             if dateTime parameter is undefined.
     */
    public static String formatDateTime(DateTime dateTime) throws IllegalArgumentException {
        LOG.trace("Format date time ({})", dateTime);

        Assert.notNull(dateTime, "Undefined date time value");

        return ISO_PERIOD_FORMATTER.print(dateTime);
    }

    /**
     * Converts given {@link DateTime} to the user friendly string representation of the local date time format.
     *
     * @param dateTime
     *            {@link DateTime} instance
     * @return the local date time formatted string (e.g. <code>2011-01-02 22:00</code>).
     * @throws IllegalArgumentException
     *             if <code>dateTime</code> parameter is undefined.
     */
    public static String formatUserDateTime(DateTime dateTime) throws IllegalArgumentException {
        LOG.trace("Format date time ({})", dateTime);

        Assert.notNull(dateTime, "Undefined date time value");

        return DATE_TIME_FORMATTER.print(dateTime);
    }

    public static String formatToFileIdentification(DateTime dateTime) throws IllegalArgumentException {
        LOG.trace("Format date time ({})", dateTime);

        Assert.notNull(dateTime, "Undefined date time value");

        return FILE_IDENTIFICATION_FORMATTER.print(dateTime);
    }

    /**
     * Converts given {@link DateTime} to the string representation of the local date format.
     *
     * @param dateTime
     *            {@link DateTime} instance
     * @return the local date formatted string (e.g. <code>2011-01-02</code>).
     * @throws IllegalArgumentException
     *             if <code>dateTime</code> parameter is undefined.
     */
    public static String formatDate(DateTime dateTime) throws IllegalArgumentException {
        LOG.trace("Format date ({})", dateTime);

        Assert.notNull(dateTime, "Undefined date time value");

        return DATE_FORMATTER.print(dateTime);
    }

    /**
     * Converts given {@link XMLGregorianCalendar} to the string representation of the local date time format.
     *
     * @param calendar
     *            {@link XMLGregorianCalendar} instance
     * @return the local date formatted string (e.g. <code>2011-01-02 22:00</code>).
     * @throws IllegalArgumentException
     *             if <code>calendar</code> parameter is undefined.
     */
    public static String formatDateTime(XMLGregorianCalendar calendar) throws IllegalArgumentException {
        LOG.trace("Format XMLGregorianCalendar ({})", calendar);

        Assert.notNull(calendar, "Undefined calendar value");

        return formatDateTime(from(calendar));
    }

    /**
     * Converts given text representation of the local date time format to the {@link DateTime}.
     *
     * @param text
     *            text representation of the local date time format (e.g. <code>2011-01-02 22:00</code>)
     * @return converted {@link DateTime} object in the local time zone.
     * @throws IllegalArgumentException
     *             if <code>text</code> parameter is undefined.
     */
    public static DateTime parseUserDateTime(String text) throws IllegalArgumentException {
        LOG.trace("Parse date ({})", text);

        Assert.notNull(text, "Undefined date time value");

        return DATE_TIME_FORMATTER.parseDateTime(text);
    }

    /**
     * Converts given text representation of the local date format to the {@link DateTime}.
     *
     * @param text
     *            text representation of the local date format (e.g. <code>2011-01-02</code>)
     * @return converted {@link DateTime} object in the local time zone.
     * @throws IllegalArgumentException
     *             if <code>text</code> parameter is undefined.
     */
    public static DateTime parseDate(String text) throws IllegalArgumentException {
        LOG.trace("Parse date ({})", text);

        Assert.notNull(text, "Undefined date time value");

        return DATE_FORMATTER.parseDateTime(text);
    }

    /**
     * Converts given text representation of the ISO date format to the {@link DateTime}.
     *
     * @param text
     *            text representation of the ISO date format (e.g. <code>2011-01-02T22:00Z</code>)
     * @return converted {@link DateTime} object in the local time zone.
     * @throws IllegalArgumentException
     *             if dateTime parameter is undefined.
     */
    public static DateTime parseDateTime(String text) throws IllegalArgumentException {
        LOG.trace("Parse date time ({})", text);

        Assert.notNull(text, "Undefined date time value");

        return ISO_PERIOD_FORMATTER.parseDateTime(text).toDateTime(DateTimeZone.getDefault());
    }

    /**
     * Converts given ISO period to the Joda Time {@link Interval} instance.
     *
     * @param isoPeriod
     *            ISO period string (e.g. <code>2011-01-02T22:00Z/2011-01-03T22:00Z</code>).
     * @return the {@link Interval} instance in the local time zone.
     * @throws IllegalArgumentException
     *             if the parameter is undefined.
     */
    public static Interval parsePeriod(String isoPeriod) throws IllegalArgumentException {
        LOG.trace("Parse period {}", isoPeriod);

        Assert.hasText(isoPeriod, "Undefined period value");

        int middle = isoPeriod.indexOf('/');

        if (middle == -1) {
            LOG.error("Invalid period format {}", isoPeriod);
            throw new RuntimeException(isoPeriod);
        }

        try {
            long start = ISO_PERIOD_FORMATTER.parseMillis(isoPeriod.substring(0, middle));
            long stop = ISO_PERIOD_FORMATTER.parseMillis(isoPeriod.substring(middle + 1));

            return new Interval(start, stop, DateTimeZone.getDefault());
        } catch (IllegalArgumentException e) {
            LOG.error("Invalid period format {}", isoPeriod, e);
            throw new RuntimeException(isoPeriod, e);
        }
    }

    public static DateTime fromDateAndTime(XMLGregorianCalendar date, XMLGregorianCalendar time) {
        LOG.trace("Parse XMLGregorianCalendar {}", date);
        LOG.trace("Parse XMLGregorianCalendar {}", time);

        Assert.notNull(date, "Undefined date value");

        if (time != null) {
            int mills = time.getMillisecond();

            return new DateTime(date.getYear(), date.getMonth(), date.getDay(),
                    time.getHour(), time.getMinute(), time.getSecond(),
                    mills == DatatypeConstants.FIELD_UNDEFINED ? 0 : mills,
                    DateTimeZone.forTimeZone(date.getTimeZone(0))).toDateTime(DateTimeZone.getDefault());
        } else {
            return new DateTime(date.getYear(), date.getMonth(), date.getDay(),
                    0, 0, 0, 0, DateTimeZone.forTimeZone(date.getTimeZone(0))).toDateTime(DateTimeZone.getDefault());
        }
    }

    public static String getInterval(DateTime startDate, DateTime stopDate) {
        if (startDate == null && stopDate == null) {
            return null;
        } else if (stopDate == null) {
            return formatPeriod(startDate, startDate);
        } else if (startDate == null) {
            return formatPeriod(stopDate, stopDate);
        }
        return formatPeriod(startDate, stopDate);
    }

    public static Interval getHoursInGap(DateTime sourceDay, DateTime targetDay) {
        DateTime sourceStart = sourceDay.withTime(0, 0, 0, 0);
        DateTime sourceEnd = sourceStart.plusDays(1);

        DateTime targetStart = targetDay.withTime(0, 0, 0, 0);
        DateTime targetEnd = targetStart.plusDays(1);

        DateTime startGap = null;
        DateTime stopGap = null;
        while (sourceStart.isBefore(sourceEnd) && targetStart.isBefore(targetEnd)) {
            if (targetStart.getHourOfDay() > sourceStart.getHourOfDay()) {
                if (startGap == null) {
                    startGap = sourceStart;
                }
                stopGap = sourceStart.plusHours(1);
                sourceStart = stopGap;
                continue;
            }
            sourceStart = sourceStart.plusHours(1);
            targetStart = targetStart.plusHours(1);
        }

        return startGap != null ? new Interval(startGap, stopGap) : null;
    }

    public static Interval getAdditionalHours(DateTime sourceDay, DateTime targetDay) {
        DateTime sourceStart = sourceDay.withTime(0, 0, 0, 0);
        DateTime sourceEnd = sourceStart.plusDays(1);

        DateTime targetStart = targetDay.withTime(0, 0, 0, 0);
        DateTime targetEnd = targetStart.plusDays(1);

        DateTime startGap = null;
        DateTime stopGap = null;
        while (sourceStart.isBefore(sourceEnd) && targetStart.isBefore(targetEnd)) {
            if (sourceStart.getHourOfDay() > targetStart.getHourOfDay()) {
                if (startGap == null) {
                    startGap = targetStart;
                }
                stopGap = targetStart.plusHours(1);
                targetStart = stopGap;
                continue;
            }

            sourceStart = sourceStart.plusHours(1);
            targetStart = targetStart.plusHours(1);
        }

        return startGap != null && stopGap != null ? new Interval(startGap, stopGap) : null;
    }

    public static DateTime getStartOfDay(DateTime dateTime) {
        return dateTime.minusMillis(dateTime.getMillisOfDay());
    }

    public static DateTime getEndOfDay(DateTime dateTime) {
        return dateTime.getMillisOfDay() > 0 ? dateTime.plusDays(1).minus(dateTime.getMillisOfDay()) : dateTime;
    }
}
